Skip to content

Prototype extension profile mapping#95

Draft
valkum wants to merge 4 commits into
djc:mainfrom
valkum:extension-profile-mapping
Draft

Prototype extension profile mapping#95
valkum wants to merge 4 commits into
djc:mainfrom
valkum:extension-profile-mapping

Conversation

@valkum

@valkum valkum commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

This is an attempt to solve #94. It supersedes #67.

This adds:

  • Profile trait, which maps a (command, request-extension tuple), for a given registry, to the response type and a tuple of response-extension types. This decouples the response extension from the request extension.
  • Multiple other traits that make this a) work and b) nicer to use at callsites.
  • RequestExts: helper trait to avoid empty elements (there might be other ways to solve this). Partially needed due to orphan rules.
  • FromExts: axum-inspired extractor to make call sites more readable.

I added tests and code examples to helper functions that show how this would look at call sites.

I also check how this looks internally, and it might fit nicely.

valkum added 4 commits June 10, 2026 14:44
Prototype for the extension-handling rework discussed in
djc#94.

A `Profile<Cmd, ReqExts>` maps a (command, request-extension tuple), for a
given registry, to the response type and a tuple of response-extension types,
decoupling the response extension from the request extension.
The existence of an impl doubles as the compile-time capability gate.

This adds `RequestExts`: a trait serializing a request-extension tuple into a
single `<extension>` element.
Verified byte-identical to the legacy `CommandWrapper` output.

`Exts<T>`: decoded `<extension>` payload as a product of options, routing
children to slots by element identity via `Deserializer::parent()`.

`EppClient<C, P = NoProfile>` gains `with_profile` and `transact_profiled`,
sourcing both response types from the profile. Existing `transact` is
untouched.
Drive the profile-driven path through the existing mock-TCP harness.
Replace the positional `exts.0 .0` tuple indexing with typed
lookup (inspired by hyper).
Add `Response::extract()` with a `FromExts` trait so several extensions can be
pulled in one destructure, with required-vs-optional encoded by type:

This grealy improves readability at callsites.

@djc djc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a quick initial round of review.

The core Profile idea seems about right? Not so sure about the further layers of stuff.

Comment thread src/client.rs

/// Marker for an [`EppClient`] that has no registry [`Profile`] attached.
#[derive(Debug)]
pub struct NoProfile;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use () for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, might be a useful change.

Comment thread src/client.rs
/// extensions based on the profile and then send them in the login.
///
/// This just helped me to quickly test the profile-driven transaction API.
pub fn with_profile<P2>(self) -> EppClient<C, P2> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this should just be part of new().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, see my remark in the comment.

Comment thread src/client.rs
/// .await;
/// }
/// ```
pub async fn transact_profiled<'c, Cmd, ReqExts>(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this replace an existing method, rather than being additive? the _profiled suffix seems pretty ugly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all of these duplicates (transact_profiled, ProfiledCommand) should replace the current ones. In this state I wanted to just see how the API feels and if it would work.

Comment thread src/profile.rs
/// The `<resData>` payload (usually just `Cmd::Response`).
type Response: FromXmlOwned;
/// The decoded `<extension>` payload — typically an [`Exts`] tuple.
type RespExts: FromXmlOwned;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's write this out as ResponseExtensions.

Comment thread src/profile.rs
/// Implemented for tuples up to arity 3. A local trait is used (rather than
/// `ToXml` on bare tuples) because orphan rules forbid implementing the foreign
/// `ToXml` trait for foreign tuple types.
pub trait RequestExts {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this RequestExtensions. Maybe this should be sealed?

Comment thread src/profile.rs
serializer.end_start()?;
self.command.serialize(None, serializer)?;

// This bit is important for strict EPP servers.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why it is important, not just that it is.

Comment thread src/profile.rs
}

impl<Cmd: ToXml, ReqExts: RequestExts> ToXml for ProfiledCommand<'_, Cmd, ReqExts> {
fn serialize<W: std::fmt::Write + ?Sized>(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: drop the std:: prefixes everywhere.

Comment thread src/profile.rs
///
/// Should store a tuple of optional response extensions.
#[derive(Debug, PartialEq)]
pub struct Exts<T>(pub T);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be private? Should not have an abbreviated name.

Comment thread src/profile.rs
}
}

impl<T: ExtTuple> Exts<T> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced of this and would prefer to postpone it into another PR.

Alternative suggestion: implement AsRef<T> for all the Ts in the tuple, which is more type safe.

Or, just don't implement this, and downstreams can implement their own named field structs with fields for the extensions? Seems pretty okay too.

Comment thread src/profile.rs
/// ```ignore
/// let (rgp, fee): (&RgpInfData, Option<&FeeData>) = rsp.extract()?;
/// ```
pub fn extract<'a, E: FromExts<'a>>(&'a self) -> Result<E, MissingExtension> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also postpone this into a separate PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants